// WoL remote for the Waveshare ESP32-S3-Touch-LCD-2.8
// libs: GFX Library for Arduino, lvgl 8.3.x

#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <Preferences.h>
#include <Arduino_GFX_Library.h>
#include <lvgl.h>

#define LCD_CS   42
#define LCD_DC   41
#define LCD_RST  39
#define LCD_BL    5
#define LCD_SCLK 40
#define LCD_MOSI 45
#define LCD_MISO 46

// touch chip is on its own i2c bus
#define TP_SDA  1
#define TP_SCL  3
#define TP_RST  2
#define TP_INT  4
#define TP_ADDR 0x1A

#define LED_PIN      -1    // -1 = no led wired up yet
#define LED_ON_LEVEL HIGH  // LOW if your led is active low

#define COL_OK   lv_color_hex(0x44DD44)
#define COL_ERR  lv_color_hex(0xFF4444)
#define COL_WAIT lv_color_hex(0xFFAA00)
#define COL_GREY lv_color_hex(0xAAAAAA)

#define W 240
#define H 320

Arduino_DataBus *bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCLK, LCD_MOSI, LCD_MISO);
Arduino_GFX *gfx = new Arduino_ST7789(bus, LCD_RST, 0, true, W, H);

static lv_disp_draw_buf_t draw_buf;
static lv_color_t *pix;

Preferences prefs;
WiFiUDP udp;

static lv_obj_t *kb;
static lv_obj_t *dd_ssid, *ta_pass, *lbl_wifi;
static lv_obj_t *ta_mac, *ta_bcast, *ta_port, *lbl_saved;
static lv_obj_t *lbl_target, *lbl_wake;
static lv_obj_t *lbl_led, *lbl_led_btn;

static String wol_mac;
static String wol_bcast = "255.255.255.255";
static int wol_port = 9;
static bool led_on = false;

static void flush_cb(lv_disp_drv_t *drv, const lv_area_t *a, lv_color_t *c) {
  uint32_t w = a->x2 - a->x1 + 1;
  uint32_t h = a->y2 - a->y1 + 1;
  gfx->draw16bitRGBBitmap(a->x1, a->y1, (uint16_t *)&c->full, w, h);
  lv_disp_flush_ready(drv);
}

// touch, CST328 over Wire1 with 16 bit register addresses
static bool tp_read(uint16_t reg, uint8_t *buf, uint8_t len) {
  Wire1.beginTransmission(TP_ADDR);
  Wire1.write(reg >> 8);
  Wire1.write(reg & 0xFF);
  if (Wire1.endTransmission(true) != 0) return false;
  if (Wire1.requestFrom((uint8_t)TP_ADDR, len) != len) return false;
  for (uint8_t i = 0; i < len; i++) buf[i] = Wire1.read();
  return true;
}

static bool tp_write(uint16_t reg, const uint8_t *buf, uint8_t len) {
  Wire1.beginTransmission(TP_ADDR);
  Wire1.write(reg >> 8);
  Wire1.write(reg & 0xFF);
  for (uint8_t i = 0; i < len; i++) Wire1.write(buf[i]);
  return Wire1.endTransmission(true) == 0;
}

static bool read_touch(uint16_t &x, uint16_t &y) {
  const uint8_t zero = 0;
  uint8_t n;
  if (!tp_read(0xD005, &n, 1)) return false;

  uint8_t points = n & 0x0F;
  if (points == 0 || points > 5) {
    tp_write(0xD005, &zero, 1);
    return false;
  }

  uint8_t d[27];
  bool ok = tp_read(0xD000, d, 27);
  tp_write(0xD005, &zero, 1);
  if (!ok) return false;

  x = (d[1] << 4) | (d[3] >> 4);
  y = (d[2] << 4) | (d[3] & 0x0F);
  if (x >= W) x = W - 1;
  if (y >= H) y = H - 1;
  return true;
}

static void touch_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) {
  static uint16_t lx, ly;
  uint16_t x, y;
  if (read_touch(x, y)) {
    lx = x;
    ly = y;
    data->state = LV_INDEV_STATE_PR;
  } else {
    data->state = LV_INDEV_STATE_REL;
  }
  data->point.x = lx;
  data->point.y = ly;
}

static void set_msg(lv_obj_t *lbl, const char *txt, lv_color_t col) {
  lv_label_set_text(lbl, txt);
  lv_obj_set_style_text_color(lbl, col, 0);
}

// invisible block at the bottom so the keyboard doesn't cover the fields
static void spacer(lv_obj_t *parent) {
  lv_obj_t *s = lv_obj_create(parent);
  lv_obj_set_size(s, 10, 120);
  lv_obj_set_style_bg_opa(s, 0, 0);
  lv_obj_set_style_border_width(s, 0, 0);
}

static void update_target() {
  String t = "Target: ";
  t += wol_mac.length() ? wol_mac : "(not set)";
  lv_label_set_text(lbl_target, t.c_str());
}

static bool send_wol() {
  String m = wol_mac;
  m.replace('-', ':');

  unsigned int v[6];
  if (sscanf(m.c_str(), "%x:%x:%x:%x:%x:%x", &v[0], &v[1], &v[2], &v[3], &v[4], &v[5]) != 6)
    return false;

  uint8_t mac[6];
  for (int i = 0; i < 6; i++) mac[i] = v[i];

  // 6x 0xFF then the mac 16 times
  uint8_t pkt[102];
  memset(pkt, 0xFF, 6);
  for (int i = 0; i < 16; i++) memcpy(pkt + 6 + i * 6, mac, 6);

  IPAddress ip;
  if (!ip.fromString(wol_bcast)) return false;

  udp.beginPacket(ip, wol_port);
  udp.write(pkt, sizeof(pkt));
  return udp.endPacket() == 1;
}

// on screen keyboard, one shared by all text fields
static void ta_event(lv_event_t *e) {
  lv_obj_t *ta = lv_event_get_target(e);
  lv_event_code_t code = lv_event_get_code(e);

  if (code == LV_EVENT_FOCUSED) {
    lv_keyboard_set_textarea(kb, ta);
    lv_obj_clear_flag(kb, LV_OBJ_FLAG_HIDDEN);
  } else if (code == LV_EVENT_DEFOCUSED || code == LV_EVENT_READY || code == LV_EVENT_CANCEL) {
    lv_keyboard_set_textarea(kb, NULL);
    lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN);
  }
}

static lv_obj_t *make_input(lv_obj_t *parent, const char *hint, bool password = false) {
  lv_obj_t *ta = lv_textarea_create(parent);
  lv_textarea_set_one_line(ta, true);
  lv_textarea_set_placeholder_text(ta, hint);
  lv_textarea_set_password_mode(ta, password);
  lv_obj_set_width(ta, LV_PCT(100));
  lv_obj_add_event_cb(ta, ta_event, LV_EVENT_ALL, NULL);
  return ta;
}

static lv_obj_t *make_button(lv_obj_t *parent, const char *text, lv_event_cb_t cb) {
  lv_obj_t *b = lv_btn_create(parent);
  lv_obj_set_width(b, LV_PCT(100));
  lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, NULL);
  lv_label_set_text(lv_label_create(b), text);
  return b;
}

// wifi tab
static void scan_cb(lv_event_t *e) {
  set_msg(lbl_wifi, "Scanning...", COL_WAIT);
  lv_timer_handler();

  WiFi.mode(WIFI_STA);
  int n = WiFi.scanNetworks();
  if (n <= 0) {
    set_msg(lbl_wifi, "No networks found", COL_ERR);
    return;
  }

  String list;
  for (int i = 0; i < n && i < 20; i++) {
    if (i) list += "\n";
    list += WiFi.SSID(i);
  }
  WiFi.scanDelete();

  lv_dropdown_set_options(dd_ssid, list.c_str());
  set_msg(lbl_wifi, "Pick a network and enter the password", COL_GREY);
}

static bool wifi_connect(const String &ssid, const String &pass) {
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid.c_str(), pass.c_str());

  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) {
    lv_timer_handler();
    delay(50);
  }
  return WiFi.status() == WL_CONNECTED;
}

static void show_connected() {
  String s = "Connected, IP " + WiFi.localIP().toString();
  set_msg(lbl_wifi, s.c_str(), COL_OK);
}

static void connect_cb(lv_event_t *e) {
  char ssid[64];
  lv_dropdown_get_selected_str(dd_ssid, ssid, sizeof(ssid));
  String pass = lv_textarea_get_text(ta_pass);

  if (ssid[0] == 0 || strcmp(ssid, "(scan first)") == 0) {
    set_msg(lbl_wifi, "Scan and pick a network first", COL_ERR);
    return;
  }

  set_msg(lbl_wifi, "Connecting...", COL_WAIT);
  if (wifi_connect(ssid, pass)) {
    prefs.begin("wifi", false);
    prefs.putString("ssid", ssid);
    prefs.putString("pass", pass);
    prefs.end();
    show_connected();
  } else {
    set_msg(lbl_wifi, "Couldn't connect, check the password", COL_ERR);
  }
}

static void build_wifi_tab(lv_obj_t *tab) {
  lv_obj_set_flex_flow(tab, LV_FLEX_FLOW_COLUMN);
  lv_obj_set_style_pad_row(tab, 8, 0);

  make_button(tab, "Scan 2.4 GHz networks", scan_cb);

  dd_ssid = lv_dropdown_create(tab);
  lv_dropdown_set_options(dd_ssid, "(scan first)");
  lv_obj_set_width(dd_ssid, LV_PCT(100));

  ta_pass = make_input(tab, "WiFi password", true);
  make_button(tab, "Connect", connect_cb);

  lbl_wifi = lv_label_create(tab);
  lv_label_set_long_mode(lbl_wifi, LV_LABEL_LONG_WRAP);
  lv_obj_set_width(lbl_wifi, LV_PCT(100));
  lv_label_set_text(lbl_wifi, "Not connected");

  spacer(tab);
}

// wol settings tab
static void save_cb(lv_event_t *e) {
  wol_mac = lv_textarea_get_text(ta_mac);
  wol_bcast = lv_textarea_get_text(ta_bcast);
  wol_port = atoi(lv_textarea_get_text(ta_port));

  if (wol_bcast.length() == 0) wol_bcast = "255.255.255.255";
  if (wol_port < 1 || wol_port > 65535) wol_port = 9;

  prefs.begin("wol", false);
  prefs.putString("mac", wol_mac);
  prefs.putString("bcast", wol_bcast);
  prefs.putInt("port", wol_port);
  prefs.end();

  update_target();
  set_msg(lbl_saved, "Saved", COL_OK);
}

static void build_wol_tab(lv_obj_t *tab) {
  lv_obj_set_flex_flow(tab, LV_FLEX_FLOW_COLUMN);
  lv_obj_set_style_pad_row(tab, 8, 0);

  lv_label_set_text(lv_label_create(tab), "MAC address");
  ta_mac = make_input(tab, "AA:BB:CC:DD:EE:FF");
  lv_textarea_set_text(ta_mac, wol_mac.c_str());

  lv_label_set_text(lv_label_create(tab), "Broadcast IP");
  ta_bcast = make_input(tab, "255.255.255.255");
  lv_textarea_set_text(ta_bcast, wol_bcast.c_str());

  lv_label_set_text(lv_label_create(tab), "UDP port");
  ta_port = make_input(tab, "9");
  lv_textarea_set_text(ta_port, String(wol_port).c_str());

  make_button(tab, "Save", save_cb);

  lbl_saved = lv_label_create(tab);
  lv_label_set_text(lbl_saved, "");

  spacer(tab);
}

// wake tab
static void wake_cb(lv_event_t *e) {
  if (WiFi.status() != WL_CONNECTED) {
    set_msg(lbl_wake, "Not on WiFi, use the WiFi tab", COL_ERR);
  } else if (wol_mac.length() == 0) {
    set_msg(lbl_wake, "No MAC set, use the WoL tab", COL_ERR);
  } else if (send_wol()) {
    set_msg(lbl_wake, "Magic packet sent", COL_OK);
  } else {
    set_msg(lbl_wake, "Send failed, check the MAC and IP", COL_ERR);
  }
}

static void build_wake_tab(lv_obj_t *tab) {
  lv_obj_set_flex_flow(tab, LV_FLEX_FLOW_COLUMN);
  lv_obj_set_flex_align(tab, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
  lv_obj_set_style_pad_row(tab, 16, 0);

  lbl_target = lv_label_create(tab);
  update_target();

  lv_obj_t *btn = lv_btn_create(tab);
  lv_obj_set_size(btn, 180, 100);
  lv_obj_set_style_radius(btn, 20, 0);
  lv_obj_set_style_bg_color(btn, lv_color_hex(0x2E9E44), 0);
  lv_obj_add_event_cb(btn, wake_cb, LV_EVENT_CLICKED, NULL);

  lv_obj_t *l = lv_label_create(btn);
  lv_label_set_text(l, LV_SYMBOL_POWER "  WAKE");
  lv_obj_set_style_text_font(l, &lv_font_montserrat_20, 0);
  lv_obj_center(l);

  lbl_wake = lv_label_create(tab);
  lv_label_set_long_mode(lbl_wake, LV_LABEL_LONG_WRAP);
  lv_obj_set_width(lbl_wake, LV_PCT(100));
  lv_obj_set_style_text_align(lbl_wake, LV_TEXT_ALIGN_CENTER, 0);
  lv_label_set_text(lbl_wake, "");
}

// test tab
static void led_cb(lv_event_t *e) {
  if (LED_PIN < 0) {
    set_msg(lbl_led, "No LED pin set, change LED_PIN at the top", COL_ERR);
    return;
  }

  led_on = !led_on;
  digitalWrite(LED_PIN, led_on ? LED_ON_LEVEL : !LED_ON_LEVEL);

  lv_label_set_text(lbl_led_btn, led_on ? LV_SYMBOL_EYE_OPEN "  LED ON" : LV_SYMBOL_EYE_CLOSE "  LED OFF");
  set_msg(lbl_led, led_on ? "LED is on" : "LED is off", led_on ? COL_OK : COL_GREY);
}

static void build_test_tab(lv_obj_t *tab) {
  lv_obj_set_flex_flow(tab, LV_FLEX_FLOW_COLUMN);
  lv_obj_set_flex_align(tab, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
  lv_obj_set_style_pad_row(tab, 16, 0);

  lv_obj_t *btn = lv_btn_create(tab);
  lv_obj_set_size(btn, 180, 80);
  lv_obj_set_style_radius(btn, 20, 0);
  lv_obj_add_event_cb(btn, led_cb, LV_EVENT_CLICKED, NULL);

  lbl_led_btn = lv_label_create(btn);
  lv_label_set_text(lbl_led_btn, LV_SYMBOL_EYE_CLOSE "  LED OFF");
  lv_obj_center(lbl_led_btn);

  lbl_led = lv_label_create(tab);
  lv_label_set_long_mode(lbl_led, LV_LABEL_LONG_WRAP);
  lv_obj_set_width(lbl_led, LV_PCT(100));
  lv_obj_set_style_text_align(lbl_led, LV_TEXT_ALIGN_CENTER, 0);
  lv_label_set_text(lbl_led, "");
}

void setup() {
  Serial.begin(115200);

  pinMode(LCD_BL, OUTPUT);
  digitalWrite(LCD_BL, HIGH);
  gfx->begin(80000000);
  gfx->fillScreen(0x0000);

  // reset the touch chip and wake it up
  pinMode(TP_INT, INPUT);
  pinMode(TP_RST, OUTPUT);
  digitalWrite(TP_RST, HIGH);
  delay(50);
  digitalWrite(TP_RST, LOW);
  delay(5);
  digitalWrite(TP_RST, HIGH);
  delay(50);
  Wire1.begin(TP_SDA, TP_SCL, 400000);

  const uint8_t z = 0;
  uint8_t cfg[24] = {0};
  tp_write(0xD101, &z, 0);
  bool ok = tp_read(0xD1F4, cfg, 24);
  Serial.printf("touch id check: 0x%02X%02X (should be CACA), read ok: %d\n", cfg[11], cfg[10], ok);
  tp_write(0xD109, &z, 0);

  lv_init();
  pix = (lv_color_t *)heap_caps_malloc(W * 40 * sizeof(lv_color_t), MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
  lv_disp_draw_buf_init(&draw_buf, pix, NULL, W * 40);

  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init(&disp_drv);
  disp_drv.hor_res = W;
  disp_drv.ver_res = H;
  disp_drv.flush_cb = flush_cb;
  disp_drv.draw_buf = &draw_buf;
  lv_disp_drv_register(&disp_drv);

  static lv_indev_drv_t indev_drv;
  lv_indev_drv_init(&indev_drv);
  indev_drv.type = LV_INDEV_TYPE_POINTER;
  indev_drv.read_cb = touch_cb;
  lv_indev_drv_register(&indev_drv);

  prefs.begin("wol", true);
  wol_mac = prefs.getString("mac", "");
  wol_bcast = prefs.getString("bcast", "255.255.255.255");
  wol_port = prefs.getInt("port", 9);
  prefs.end();

  lv_obj_t *tabs = lv_tabview_create(lv_scr_act(), LV_DIR_TOP, 36);
  build_wifi_tab(lv_tabview_add_tab(tabs, "WiFi"));
  build_wol_tab(lv_tabview_add_tab(tabs, "WoL"));
  build_wake_tab(lv_tabview_add_tab(tabs, "Wake"));
  build_test_tab(lv_tabview_add_tab(tabs, "Test"));

  if (LED_PIN >= 0) {
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, !LED_ON_LEVEL);
  }

  kb = lv_keyboard_create(lv_layer_top());
  lv_obj_set_size(kb, W, 140);
  lv_obj_align(kb, LV_ALIGN_BOTTOM_MID, 0, 0);
  lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN);

  // reconnect to the saved network if there is one
  prefs.begin("wifi", true);
  String ssid = prefs.getString("ssid", "");
  String pass = prefs.getString("pass", "");
  prefs.end();

  if (ssid.length()) {
    set_msg(lbl_wifi, "Connecting to saved network...", COL_WAIT);
    lv_timer_handler();
    if (wifi_connect(ssid, pass)) show_connected();
    else set_msg(lbl_wifi, "Saved network not reachable", COL_ERR);
  }
}

void loop() {
  static uint32_t last = millis();
  uint32_t now = millis();
  lv_tick_inc(now - last);
  last = now;

  lv_timer_handler();
  delay(5);
}
